FreeRTOS 临界区

在并行编程中,并行存取共享的资源时,常常会导致意外和错误的结果。例如下面代码task1和task2都通过串口打印消息,但由于任务调度,消息被截断了。

1#include <stm32f4xx.h>
2#include <FreeRTOS.h>
3#include <task.h>
4#include <uart.h>
5 
6void task1(void* args);
7void task2(void* args);
8 
9int main()
10{
11    //配置USART1
12    USART1_Config();
13    //创建任务
14    TaskHandle_t h1,h2;
15    xTaskCreate(task1,"task1",configMINIMAL_STACK_SIZE,NULL,1,&h1);
16    xTaskCreate(task2,"task2",configMINIMAL_STACK_SIZE,NULL,1,&h2);
17    //开启任务调度
18    vTaskStartScheduler();
19    while(1);
20}
21 
22 
23void task1(void* args)
24{
25    USART_printf(USART1,"task1 is running.\n");
26    vTaskDelete(NULL);
27}
28 
29void task2(void* args)
30{
31    USART_printf(USART1,"task2 is running.\n");
32    vTaskDelete(NULL);
33}

Image

类似的,当修改公共变量时任务调度可能导致变量的值错误、操作时序严格的设备时任务调度可能导致设备不能正常工作。

因此,访问共享资源的部分代码会被保护起来,在执行这段代码时不进行任务调度,这样的代码段称为临界区(Critical Section)

FreeRTOS通过两个包含在task.h里的宏taskENTER_CRITICAL()taskEXIT_CRITICAL()来进入和离开临界区在。taskENTER_CRITICAL()之后、taskEXIT_CRITICAL()之前不会切换到其他任务。

修改USART_printf函数,在打印消息前后加入taskENTER_CRITICAL()taskEXIT_CRITICAL(),这样在USART_printf运行期间就不会切换任务,打印的消息也就不会被截断。

1#define USART_PRINTF_BUFFER_SIZE 256
2 
3int USART_printf(USART_TypeDef* port,const char* fmt,...)
4{
5    char str[USART_PRINTF_BUFFER_SIZE];
6    int length,index;
7    va_list argList;
8    va_start(argList,fmt);
9    length = vsnprintf(str,USART_PRINTF_BUFFER_SIZE-1,fmt,argList);
10    va_end(argList);
11    taskENTER_CRITICAL();//进入临界区
12    for(index = 0 ; index < length ; index++)
13    {
14        USART_WriteByte(port,str[index]);
15    }
16    taskEXIT_CRITICAL();//离开临界区
17    return length;
18}

Image

临界区嵌套是安全的,因为内核有维护一个嵌套深度计数。临界区只会在嵌套深度为0时才会真正退出——即在为每个之前调用的taskENTER_CRITICAL()都配套调用了taskEXIT_CRITICAL()之后。

临界区是互斥功能的一种非常原始的实现方式,通常只是关闭所有中断从而使任务调度暂停。这样可能会导致任务不能正常执行,中断不能及时响应等问题。所以临界区应当只具有很短的时间。上例中打印串口消息是一个相当耗费时间的过程,并不适合使用临界区。

另外也可以将调度器挂起来保护临界区,vTaskSuspendAll挂起调度器、xTaskResumeAll恢复调度器,在vTaskSuspendAllxTaskResumeAll之间不会进行任务调度,但可以响应中断。

1#include <task.h>
2void vTaskSuspendAll(void);
3BaseType_t xTaskResumeAll(void);